package generator.workflow.sfj.gridgain.j2xml_impl;

import generator.datastructure.MethodDeclaration;
import generator.datastructure.WritebleClass;
import generator.datastructure.j2xml_impl.DocumentWrapper;
import generator.util.DocTransformer;
import generator.util.J2XMLManipulator;
import generator.util.input_interpreter.ParallelizerInterpreter;
import generator.workflow.sfj.gridgain.GridGainBindingManager;

import java.io.File;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class BindingManagerJ2XML extends GridGainBindingManager {

	
	/**
	 * Documento que representa la clase clase java convencional codificada por el usuario.
	 * El mismo constituye el producto base sobre el cual este builder trabaja para lograr
	 * la gridificación de los métodos indicados por el usuario. 	  
	 */
	protected org.w3c.dom.Document bindedClass = null;

	/**Documento del que se extraen estructuras pre armadas a utilizar durante la generación
	 * de la clase peer*/
	protected org.w3c.dom.Document preBuildStructures;
	
	/**Objeto utilizado para evaluar expresiones xpath sobre archivos xml*/
	private XPath xpath;
	
	/**Objeto que se utilizará para realizar operaciones genéricas sobre clases java
	 * representadas como documentos que responden al formato impuesto por la biblioteca
	 * JavaToXml*/
	private J2XMLManipulator cm;
	
	
	
	/**Constructor de la clase en la que se inicializan las variables que la misma utiliza para
	 * la generación de la clase peer. 
	 * @param inputReader
	 * 			provee información la ruta de la clase que contiene los métodos a gridificar*/
	public BindingManagerJ2XML(ParallelizerInterpreter inputReader){
		cm = new J2XMLManipulator();
		xpath = XPathFactory.newInstance().newXPath();
		this.pi = inputReader;
		preBuildStructures = DocTransformer.xmlToDom(new File(cm.getPreBuildStructuresPath()));		
		this.bindedClass = J2XMLManipulator.classToDOM(inputReader.getClassToParallelize());						
	}
	
	
	/** {@inheritDoc} */
	@Override
	protected void startUpGrid(MethodDeclaration md) {
		try {			
			Element startGridSentence = (Element)xpath.evaluate("//structure[@id='startGrid']/*", preBuildStructures, XPathConstants.NODE);
			startGridSentence = (Element)this.bindedClass.importNode(startGridSentence, true);
			//Seteo el nombre de la clase Peer
			String peerClassName = md.getClassOwner().substring(md.getClassOwner().lastIndexOf(".")+1)+"Peer";
			Element peerInstance =  (Element)xpath.evaluate("self::*//"+cm.VARREF, startGridSentence, XPathConstants.NODE);
			peerInstance.setAttribute("name", peerClassName);
			// Agrego el Element
			//self::*/block/node()[1]
			Element firstSentence = (Element)xpath.evaluate("self::*/"+cm.BODYMETHOD+"/*[1]", cm.getMethodImpl(md, bindedClass), XPathConstants.NODE);			
			firstSentence.getParentNode().insertBefore(startGridSentence, firstSentence);			
			
		} catch (XPathExpressionException e) {
			System.err.println("Falló al tratar de insertar la sentencia de creación del grid");
			e.printStackTrace();
		}		
	}
	
	/** {@inheritDoc} */
	@Override
	protected void stopGrid(MethodDeclaration md) {
		try {			
			Element stopGridSentence = (Element)xpath.evaluate("//structure[@id='stopGrid']/*", preBuildStructures, XPathConstants.NODE);
			stopGridSentence = (Element)this.bindedClass.importNode(stopGridSentence, true);
			//Seteo el nombre de la clase Peer
			String peerClassName = md.getClassOwner().substring(md.getClassOwner().lastIndexOf(".")+1)+"Peer";
			Element peerInstance =  (Element)xpath.evaluate("self::*//"+cm.VARREF, stopGridSentence, XPathConstants.NODE);
			peerInstance.setAttribute("name", peerClassName);
			// Agrego el Element
			Element returnSentence = (Element)xpath.evaluate("self::*//"+cm.RETURN, cm.getMethodImpl(md, bindedClass), XPathConstants.NODE);			
			returnSentence.getParentNode().insertBefore(stopGridSentence, returnSentence);			
		} catch (Exception e) {
			System.err.println("Falló inserción de la sentencia de stop del grid. Verifique que la sentencia de retorno de resultado exista");
			e.printStackTrace();
		}		

	}
	
	/**Inserta en el método stub (dentro de la clase original del usuario) la sentencia de invocación del metodo gridificado sobre la clase Peer
	 * y la sentencia de retorno del resultado. Ej: <P>
	 * long retorno = peerInstance.fibonacci(i);
	 * return retorno;
	 * <P>
	 * Donde peerInstance es una instancia de la clase peer que se crea e inicializa en pasos previos del proceso de binding.
	 * @see createPeerClassInstance(WritebleClass peerClass, MethodDeclaration md) y copyProperties(WritebleClass peerClass, MethodDeclaration md)
	 * @param md
	 * 			elemento que representa la signatura del método que recibirá las transformaciones que hagan posible la ligadura de la clase original
	 * 			del usuario con la clase peer. 
	 * */
	@Override	
	protected void insertPeerClassCall(MethodDeclaration md) {
		try {
			Element methodBlock =  (Element)xpath.evaluate("self::*/"+cm.BODYMETHOD+"[1]", cm.getMethodImpl(md, bindedClass), XPathConstants.NODE);
			//agrego la sentencia de invocación de la clase peer
			Element peerInvocation = getPeerInvocation(md);
			methodBlock.appendChild(peerInvocation);
			//agrego la sentencia de retorno del resultado			
			Element returnSentence =  getReturnSentece(peerInvocation.getAttribute("name"));						
			methodBlock.appendChild(returnSentence);			
		} catch (XPathExpressionException e) {
			System.err.println("Falló inserción de la sentencia de llamado a la clase peer");
			e.printStackTrace();
		}		
	}

	/**Arma e importa una sentencia de retorno de la variable cuyo nombre se inidica en el parámetro
	 * @param attribute
	 * 			nombre de la variable que será devuelta en la sentencia return
	 * @return elemento que representa una sentencia return de de una variable cuyo nombre se indica en el parámetro del método. Ej: <BR>
	 * return x;<BR>
	 * NOTA: el método realiza la importación del nodo que retorna en el documento que representa la clase original: {@link bindedClass}. Sin
	 * embargo la ubicación del elemento dentro del documento queda a cargo de quien invoque a este método.*/
	private Element getReturnSentece(String attribute) {
		Element returnSentence = null;
		try {
			returnSentence = (Element)xpath.evaluate("//structure[@id='returnLocalVariable']/*", preBuildStructures, XPathConstants.NODE);
			returnSentence = (Element)this.bindedClass.importNode(returnSentence, true);
			Element var = (Element)xpath.evaluate("self::*/"+cm.VARREF, returnSentence, XPathConstants.NODE);
			var.setAttribute("name", attribute);			
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}		
		return returnSentence;
	}

	/**Arma la sentencia que corresponde al llamado de la clase peer. Dicho llamado a su vez se almacena en una variable local del tipo
	 * devuelto por el método a gridificar.
	 * @param md
	 * 			representa la signatura del método que desea gridificarse.
	 * @return un elemento que representa la declaración y asignación de valor de una variable. El tipo de dicha variable cohincide con el 
	 * devuelto por el método a gridificar y el valor que se le asigna proviene del llamado al método gridificado de la clase peer.
	 * Ej:<P>
	 * long retorno = peerInstance.fibonacci(i);
	 * <BR>NOTA: el método realiza la importación del nodo devuelto pero no la ubicación del mismo dentro del documento que representa la
	 * clase original: {@link bindedClass}*/
	private Element getPeerInvocation(MethodDeclaration md) {
		Element peerInvocation = null;
		try {
			peerInvocation = (Element)xpath.evaluate("//structure[@id='gridifyMethodCall']/*", preBuildStructures, XPathConstants.NODE);		
			peerInvocation = (Element)this.bindedClass.importNode(peerInvocation, true);			
			//seteo el tipo de la variable que alojará el resultado del llamado gridificado
			Element returnType = cm.parseType(md.getReturnType()); 
			cm.setType(returnType, peerInvocation, "self::*//"+cm.TYPE);			
			//seteo el nombre del método a gridificar			
			Element call = (Element)xpath.evaluate("self::*//"+cm.METHODCALL, peerInvocation, XPathConstants.NODE);
			call.setAttribute("message", md.getMethodName());
			//seteo de los argumentos del llamado. Obtengo los nombres de la declaración del método con la expresion: self::*/formal-arguments/*			
			NodeList formalArguments = (NodeList)xpath.evaluate("self::*/"+cm.ALLPARAMS+"/*", cm.getMethodImpl(md, bindedClass), XPathConstants.NODESET);
			Element realArguments =  (Element)xpath.evaluate("self::*//"+cm.METHODCALL+"/"+cm.ARGUMENTS, peerInvocation, XPathConstants.NODE);
			for(int i=0; i < formalArguments.getLength(); i++){
				realArguments.appendChild(cm.createNewTag(peerInvocation.getOwnerDocument(), "var-ref", ((Element)formalArguments.item(i)).getAttribute("name")));
			}		
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}
		return peerInvocation;
	}

	/** {@inheritDoc} */
	@Override
	protected void copyProperties(MethodDeclaration md) {		
		try {
			NodeList fields = (NodeList)xpath.evaluate("//"+cm.ATTRIBUTE, bindedClass, XPathConstants.NODESET);
			Element methodBlock = (Element)((NodeList)cm.getMethodImpl(md,this.bindedClass).getElementsByTagName(cm.BODYMETHOD)).item(0);
			for (int i = 0; i < fields.getLength(); i++) {
				Element field = (Element)fields.item(i);
				if(!field.getAttribute("name").equals("hash")){
					Element setAtrib = (Element)xpath.evaluate("//structure[@id='setPeerProperty']/*", preBuildStructures, XPathConstants.NODE);
					setAtrib = (Element)this.bindedClass.importNode(setAtrib, true);
					Element getFunction = (Element)((NodeList)xpath.evaluate("//"+cm.METHODCALL,setAtrib,XPathConstants.NODESET)).item(0);				
					setAtrib.setAttribute("message","set"+this.toProperCase(field.getAttribute("name")));
					getFunction.setAttribute("message","get"+this.toProperCase(field.getAttribute("name")));
					methodBlock.appendChild(setAtrib);
				}				
			}			
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Método que pasa a Proper case el string que recibe como parametro
	 * @param string
	 * @return
	 */
	private String toProperCase(String string){
		String initial = string.substring(0, 1).toUpperCase();
		string= initial + string.substring(1,string.length());
		return string;
	}
	

	/** {@inheritDoc} */
	@Override
	protected void createPeerClassInstance(MethodDeclaration md) {
		// Nombre del tipo de clase Peer de la cual voy a crear una nueva instancia 
		String peerClassName = md.getClassOwner().substring(md.getClassOwner().lastIndexOf(".")+1)+"Peer";
		try {
			// Obtengo y reemplazo el nuevo bloque a reemplazar en el metodo a gridificar
			Element newBlock= (Element)xpath.evaluate("//structure[@id='newBlockTag']/*", preBuildStructures, XPathConstants.NODE);
			Element method = cm.getMethodImpl(md, this.bindedClass);
			Element oldBlock = (Element)xpath.evaluate("self::*/"+cm.BODYMETHOD, method,XPathConstants.NODE);
			newBlock = (Element)bindedClass.importNode(newBlock, true);
			// Seteo el tipo de la instancia Peer que creo
			Element classType = (Element)newBlock.getElementsByTagName("type").item(0);
			Element newType = (Element)newBlock.getElementsByTagName("type").item(1);			
			classType.setAttribute("name",peerClassName);
			newType.setAttribute("name",peerClassName);
			method.replaceChild(newBlock, oldBlock);			
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}
	}

	/** Método q retorna la clase original pero con el método a gridificar cableado al middleware objetivo
	 */
	@Override
	protected WritebleClass getStubClass() {		
		return new DocumentWrapper(bindedClass);
	}

}
